package org.nativejpa;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
import org.enhydra.jdbc.pool.StandardXAPoolDataSource;

import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceException;
import javax.sql.DataSource;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Properties;
import java.net.URL;

import org.nativejpa.mapping.EntityRepository;
import org.nativejpa.query.EntityHolder;
import org.nativejpa.query.NamedSQLTemplate;
import org.nativejpa.query.QueryRepository;
import org.nativejpa.cache.CacheConfig;
import org.nativejpa.transaction.TransactionManagerFactory;
import org.nativejpa.transaction.AtomikosTransactionManagerFactory;
import org.nativejpa.parser.PersistenceXML;
import org.nativejpa.parser.PersistenceUnit;
import org.nativejpa.datasource.DataSourceFactory;
import org.nativejpa.datasource.AtomikosXADataSourceFactory;
import org.nativejpa.util.AnnotationRepository;
import org.nativejpa.util.AnnotationUtils;

/**
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 * @create 2009-3-17 16:21:57
 */
public class EntityManagerFactoryBuilder {
    protected static Log log = LogFactory.getLog(EntityManagerFactoryBuilder.class);

    private final static String PERSISTENCE_CONFIG_FILE = "META-INF/persistence.xml";

    private static EntityManagerFactoryBuilder instance;

    /**
     * Entity Manager Factory Map
     * Map (unit name & parameter properties) => EntityManagerFactoryImpl
     */
    private Map<Map, EntityManagerFactoryImpl> emFactoryMap = new HashMap<Map, EntityManagerFactoryImpl>();

    private boolean containerManaged = false;

    public final String DEFAULT_UNITNAME = "default";

    private PersistenceXML persistenceXML;

    /**
     * entityHolder used to hold all entity classes
     * EntityHolder can be set by thirdparty framework in integration 
     */

    private EntityHolder entityHolder;

    public synchronized static EntityManagerFactoryBuilder getInstance(){
        if(instance == null) {
            instance = new EntityManagerFactoryBuilder();
        }
        return instance;
    }

    private EntityManagerFactoryBuilder() {
        URL url = this.getClass().getClassLoader().getResource(PERSISTENCE_CONFIG_FILE);
        if (url == null) {
            log.warn("Can not find any persistence config file: " + PERSISTENCE_CONFIG_FILE);
            return;
        }
        log.info("Loading persistence.xml by " + url.toString());
        persistenceXML = PersistenceXML.loadPersistenceXML();
    }

    public void loadEntities(){
        // default use Annotation Scanner
        if(entityHolder == null) {
            setEntityHolder(new EntityHolder(){
                public Class<?>[] getEntityClasses() {
                    AnnotationRepository annotationRepository = new AnnotationRepository((URLClassLoader)this.getClass().getClassLoader());
                    annotationRepository.scanAnnotation(".*");
                    return annotationRepository.getAnnotatedClasses(Entity.class);
                }
            });
        }

        EntityRepository.getInstance().parseEntities(getEntityHolder().getEntityClasses());
        QueryRepository.getInstance().parseEntities(getEntityHolder().getEntityClasses());

        Set<String> mappingFiles = new HashSet<String>(8);
        for(PersistenceUnit punit : persistenceXML.getAllPersistenceUnits()){
            mappingFiles.addAll(Arrays.asList(punit.getMappingFiles()));
        }
        QueryRepository.getInstance().parseMappingFiles(mappingFiles.toArray(new String[mappingFiles.size()]));

    }

    /**
     * 容器外运行时，通过 Persistence.createEntityManagerFactory 调用时，会使用该方法
     * 需要初始化 EntityManagerFactoryBuilder，注册所有 NamedNativeQuery
     *
     * @param persistenceUnitName unit name
     */
    public synchronized EntityManagerFactoryImpl createEntityManagerFactory(String persistenceUnitName, Map properties) {
        Properties keyMap = new Properties();
        keyMap.put("__persistence_unit_name__", persistenceUnitName);
        if(properties != null && !properties.isEmpty()) {
            keyMap.putAll(properties);
        }
        if(!emFactoryMap.containsKey(keyMap)) {
            PersistenceUnit persistenceUnit = persistenceXML.getPersistenceUnit(persistenceUnitName);
            if(persistenceUnit == null) {
                throw new PersistenceException("Persistence Unit " + persistenceUnitName + " does NOT exist.");
            }
            persistenceUnit = persistenceUnit.clonePersistenceUnit();
            persistenceUnit.addProperties(keyMap);
            log.info("Creating EntityManagerFactory for unit: " + persistenceUnitName + " with properties: " + (properties == null ? "null" : properties.toString()));
            EntityManagerFactoryImpl emFactory = createEntityManagerFactory(persistenceUnit);
            emFactoryMap.put(keyMap, emFactory);
        }

        EntityManagerFactoryImpl emf =  emFactoryMap.get(keyMap);
        if(!emf.isOpen()) { // may closed by other thread
            emf.reOpen();
        }
        return emf;        
    }

    public EntityManagerFactory getDefaultEntityManagerFactory() {
        if (emFactoryMap.size() != 1) {
            throw new PersistenceException("More than one unitName, can not decide default!");
        }
        return emFactoryMap.values().toArray(new EntityManagerFactory[1])[0];
    }

    public Collection<EntityManagerFactoryImpl> getEntityManagerFactories() {
        return Collections.unmodifiableCollection(emFactoryMap.values());
    }

    /**
     * 使用 @Resource 未指定 name 注入
     */
    public DataSource getDefaultDataSource() {
        return ((EntityManagerFactoryImpl)getDefaultEntityManagerFactory()).getDataSource();
    }

    /**
     * 使用 @Resource 指定 name 注入
     *
     * @param unitName unit name, same as @resource name
     */
    private DataSource getDataSourceByUnitName(String unitName) {
        EntityManagerFactoryImpl emf = emFactoryMap.get(unitName);
        if (emf == null) {
            throw new PersistenceException("Can not find DataSource with unitName: " + unitName);
        }
        else {
            return emf.getDataSource();
        }
    }

    /**
     * get data source by Mapped Name, if inject by @PersistenceContext(mappedName="")
     *
     * @param mappedName mapped name, same as jndi name
     */
    private DataSource getDataSourceByMappedName(String mappedName) {
        for (EntityManagerFactory emf : emFactoryMap.values()) {
            StandardXAPoolDataSource dataSource = (StandardXAPoolDataSource)(((EntityManagerFactoryImpl)emf).getDataSource());
            if ((dataSource.getDataSourceName().equals(mappedName))) {
                return dataSource;
            }
        }
        throw new PersistenceException("Can not find DataSource with mappedName: " + mappedName);
    }

    //TOOD: get CacheConfig by unit & cacheConfigName
    private CacheConfig getCacheConfig(String unitName) {
        EntityManagerFactoryImpl emf = createEntityManagerFactory(unitName, null);
        if (emf != null) {
            return emf.getCacheConfig();
        }
        else {
            return null;
        }
    }

    public boolean isContainerManaged() {
        return containerManaged;
    }

    private EntityManagerFactoryImpl createEntityManagerFactory(PersistenceUnit persistenceUnit) {
        return new EntityManagerFactoryImpl(persistenceUnit, this);
    }

    public URL getPersistenceXML() {
        return Thread.currentThread().getContextClassLoader().getResource(PersistenceXML.PERSISTENCE_XML_FILENAME);
    }

    public TransactionManagerFactory getTransactionManagerFactory() {
        return new AtomikosTransactionManagerFactory();
    }

    NamedSQLTemplate getNamedSQLTemplate(String name) {
        return QueryRepository.getInstance().getNamedSQLTemplate(name);
    }

    public void setEntityHolder(EntityHolder entityHolder){
        this.entityHolder = entityHolder;
    }

    public EntityHolder getEntityHolder() {
        return entityHolder;
    }
}
